<?php
declare(strict_types=1);

namespace App\Crontab;

use App\Cache\CacheKey;
use App\Model\SystemInformation;
use Hyperf\Crontab\Annotation\Crontab;
use Hyperf\DbConnection\Db;
use Hyperf\DbConnection\Pool\PoolFactory;
use Hyperf\Logger\LoggerFactory;
use Hyperf\Redis\Redis;
use Hyperf\Server\ServerInterface;
use Hyperf\Utils\ApplicationContext;
use Hyperf\Utils\Coroutine;
use Psr\Log\LoggerInterface;

#[Crontab(name: 'SystemMonitoringCrontab', rule: '* * * * *', callback: 'execute', memo: '每1分钟收集一次系统数据')]
class SystemMonitoringCrontab
{
    private string $os;

    private LoggerInterface $logger;

    public function __construct(private LoggerFactory $loggerFactory, private Redis $redis)
    {
        $this->os = strtoupper(substr(PHP_OS, 0, 3));
        $this->logger = $this->loggerFactory->get('default');
    }

    public function execute()
    {
        $cpuUsage = $this->getCpuUsage();
        $memoryUsage = $this->getMemoryUsage();
        $diskUsage = $this->getDiskUsage();
        $conn = 0;
        $dbConn = 0;

        $app = ApplicationContext::getContainer();
        try {
            $server = $app->get(ServerInterface::class);
            $stats = $server->getServer()->stats();
            $conn = $stats['connection_num'] ?? 0;
        } catch (\Throwable $exception) {
            $this->logger->error('获取服务器连接数失败: ' . $exception->getMessage());
        }

        try {
            $dbPoolFactory = $app->get(PoolFactory::class);
            $dbPool = $dbPoolFactory->getPool('default');
            $dbConn = $dbPool->getCurrentConnections();
        } catch (\Throwable $exception) {
            $this->logger->error('获取数据库连接池信息失败: ' . $exception->getMessage());
        }

        $redisMemInfo = $this->redis->rawCommand('INFO', 'MEMORY');
        $redisInfo = $this->parseMemoryInfo($redisMemInfo);

        $mysqlQPS = $this->getMySqlQPS();

        $rpm = $this->getRPM();
        $errorCount = $this->getErrorCount();

        SystemInformation::create([
            'cpu_usage_rate' => $cpuUsage,
            'memory_usage' => $memoryUsage['used'],
            'memory_total' => $memoryUsage['total'],
            'disk_usage' => $diskUsage['used'],
            'disk_total' => $diskUsage['total'],
            'active_conn' => $conn,
            'mysql_conn' => $dbConn,
            'mysql_conn_total' => config('database.default.pool.max_connections'),
            'redis_memory_usage' => $redisInfo['used_memory'],
            'redis_memory_total' => $redisInfo['maxmemory'],
            'mysql_qps' => $mysqlQPS,
            'active_users' => $this->getActiveUsers(),
            'rpm' => $rpm,
            'error_rate' => $errorCount / $rpm,
        ]);
    }

    public function getMySqlQPS()
    {
        $result1 = Db::select("SHOW GLOBAL STATUS LIKE 'Questions'");
        $questions1 = (int)$result1[0]->Value;
        $time1 = microtime(true);

        Coroutine::sleep(1);

        $result2 = Db::select("SHOW GLOBAL STATUS LIKE 'Questions'");
        $questions2 = (int)$result2[0]->Value;
        $time2 = microtime(true);

        $timeDiff = $time2 - $time1;
        return $timeDiff > 0 ? ceil(($questions2 - $questions1) / $timeDiff) : 0;
    }

    public function getCpuUsage()
    {
        if ($this->os === 'LIN') {
            return $this->getLinuxCpuUsage();
        } elseif ($this->os === 'DAR') {
            return $this->getMacCpuUsage();
        } elseif ($this->os === 'WIN') {
            return $this->getWindowsCpuUsage();
        } else {
            return $this->getUnixCpuUsage();
        }
    }

    public function getMemoryUsage()
    {
        if ($this->os === 'LIN') {
            return $this->getLinuxMemory();
        } elseif ($this->os === 'DAR') {
            return $this->getMacMemory();
        } elseif ($this->os === 'WIN') {
            return $this->getWindowsMemory();
        } else {
            return $this->getUnixMemory();
        }
    }

    public function getDiskUsage()
    {
        if ($this->os === 'LIN') {
            return $this->getLinuxDisk();
        } elseif ($this->os === 'DAR') {
            return $this->getMacDisk();
        } elseif ($this->os === 'WIN') {
            return $this->getWindowsDisk();
        } else {
            return $this->getUnixDisk();
        }
    }

    private function getLinuxDisk(): array {
        $df = shell_exec('df -BG --output=size,used,avail,pcent /');
        preg_match('/\s*(\d+)G\s+(\d+)G\s+(\d+)G\s+(\d+)%/', $df, $matches);

        return [
            'total' => (float)$matches[1],
            'used' => (float)$matches[2],
            'free' => (float)$matches[3],
            'usage_percent' => (float)$matches[4]
        ];
    }

    private function getMacDisk(): array {
        $df = shell_exec('df -g /');
        preg_match('/\s*(\d+)Gi\s+(\d+)Gi\s+(\d+)Gi\s+(\d+)%/', $df, $matches);

        return [
            'total' => (float)$matches[1],
            'used' => (float)$matches[2],
            'free' => (float)$matches[3],
            'usage_percent' => (float)$matches[4]
        ];
    }

    private function getWindowsDisk(): array {
        $psCommand = 'Get-PSDrive C | Select-Object Used,Free | ForEach-Object { ' .
            '[math]::Round($_.Used/1GB,2), [math]::Round($_.Free/1GB,2) }';
        $output = shell_exec("powershell -command \"$psCommand\"");
        preg_match('/(\d+\.?\d*)\s+(\d+\.?\d*)/', $output, $matches);

        $used = (float)$matches[1];
        $free = (float)$matches[2];
        $total = $used + $free;

        return [
            'total' => round($total, 2),
            'used' => round($used, 2),
            'free' => round($free, 2),
            'usage_percent' => round($used / $total * 100, 2)
        ];
    }

    private function getUnixDisk(): array {
        if (@is_executable('/bin/df')) {
            $df = shell_exec('df -g /');
            if (preg_match('/\s*(\d+)G\s+(\d+)G\s+(\d+)G\s+(\d+)%/', $df, $matches)) {
                return [
                    'total' => (float)$matches[1],
                    'used' => (float)$matches[2],
                    'free' => (float)$matches[3],
                    'usage_percent' => (float)$matches[4]
                ];
            }
        }

        throw new \RuntimeException('Cannot determine disk usage on this Unix system');
    }

    private function getLinuxCpuUsage()
    {
        $prevStat = file('/proc/stat');
        Coroutine::sleep(1);
        $stat = file('/proc/stat');

        $info1 = explode(' ', preg_replace('!cpu +!', '', $prevStat[0]));
        $info2 = explode(' ', preg_replace('!cpu +!', '', $stat[0]));

        $dif = [];

        for ($i = 0; $i < count($info1); $i++) {
            $dif[$i] = $info2[$i] - $info1[$i];
        }

        $total = array_sum($dif);

        $cpu = 100 * ($total - $dif[3]) / $total;

        return number_format($cpu, 2);
    }

    private function getMacCpuUsage()
    {
        $output = shell_exec('top -l 1 -n 0 | grep -E "^CPU"');
        if (preg_match('/CPU usage: (\d+\.\d+)% user, (\d+\.\d+)% sys, (\d+\.\d+)% idle/', $output, $matches)) {
            return (float)$matches[1] + (float)$matches[2];
        }
        return 0.0;
    }

    private function getWindowsCpuUsage()
    {
        $output = shell_exec('wmic cpu get loadpercentage');
        if (preg_match('/(\d+)/', $output, $matches)) {
            return (float)$matches[1];
        }
        return 0.0;
    }

    private function getUnixCpuUsage()
    {
        $load = sys_getloadavg();
        $cores = (int)shell_exec('nproc');
        return ($load[0] / $cores) * 100;
    }

    private function getLinuxMemory(): array {
        $data = file_get_contents('/proc/meminfo');
        preg_match_all('/^(MemTotal|MemFree|Buffers|Cached):\s+(\d+)\s+kB$/m', $data, $matches);

        $mem = array_combine($matches[1], $matches[2]);
        $total = $mem['MemTotal'] / 1024 / 1024;
        $free = ($mem['MemFree'] + $mem['Buffers'] + $mem['Cached']) / 1024 / 1024;
        $used = $total - $free;

        return [
            'total' => $total,
            'used' => $used,
            'free' => $free,
        ];
    }

    private function getMacMemory(): array
    {
        $total = (int)shell_exec('sysctl -n hw.memsize');
        $total = $total / 1024 / 1024 / 1024;
        $vm = shell_exec('vm_stat');

        preg_match('/Pages free:\s+(\d+)\./', $vm, $freeMatches);
        preg_match('/Pages active:\s+(\d+)\./', $vm, $activeMatches);
        preg_match('/Pages inactive:\s+(\d+)\./', $vm, $inactiveMatches);
        preg_match('/Pages wired down:\s+(\d+)\./', $vm, $wiredMatches);

        $pageSize = (int)shell_exec('pagesize') / 1024 / 1024 / 1024;
        $free = $freeMatches[1] * $pageSize;
        $used = ($activeMatches[1] + $inactiveMatches[1] + $wiredMatches[1]) * $pageSize;

        return [
            'total' => $total,
            'used' => $used,
            'free' => $free
        ];
    }

    private function getWindowsMemory(): array {
        $output = shell_exec('wmic OS get TotalVisibleMemorySize,FreePhysicalMemory /Value');
        preg_match('/TotalVisibleMemorySize=(\d+)/', $output, $totalMatches);
        preg_match('/FreePhysicalMemory=(\d+)/', $output, $freeMatches);

        $total = $totalMatches[1] / 1024 / 1024;
        $free = $freeMatches[1] / 1024 / 1024;
        $used = $total - $free;

        return [
            'total' => round($total, 2),
            'used' => round($used, 2),
            'free' => round($free, 2)
        ];
    }

    private function getUnixMemory(): array {
        if (@is_readable('/usr/bin/free')) {
            $free = shell_exec('free -g | grep Mem');
            if (preg_match('/Mem:\s+(\d+)\s+(\d+)\s+(\d+)/', $free, $matches)) {
                return [
                    'total' => (float)$matches[1],
                    'used' => (float)$matches[2],
                    'free' => (float)$matches[3]
                ];
            }
        }

        if (@is_readable('/usr/sbin/sysctl')) {
            $total = shell_exec('sysctl -n hw.physmem') / 1024 / 1024 / 1024;
            $inactive = shell_exec('sysctl -n vm.stats.vm.v_inactive_count') * shell_exec('sysctl -n hw.pagesize') / 1024 / 1024 / 1024;
            $cache = shell_exec('sysctl -n vm.stats.vm.v_cache_count') * shell_exec('sysctl -n hw.pagesize') / 1024 / 1024 / 1024;
            $free = shell_exec('sysctl -n vm.stats.vm.v_free_count') * shell_exec('sysctl -n hw.pagesize') / 1024 / 1024 / 1024;

            return [
                'total' => round($total, 2),
                'used' => round($total - ($free + $inactive + $cache), 2),
                'free' => round($free + $inactive + $cache, 2)
            ];
        }

        throw new \RuntimeException('Cannot determine memory usage on this Unix system');
    }

    private function parseMemoryInfo(string $info): array
    {
        $data = [];
        foreach (explode("\r\n", $info) as $line) {
            if (str_contains($line, ':')) {
                [$key, $value] = explode(':', $line, 2);
                $data[$key] = $value;
            }
        }
        return [
            'used_memory' => (int)$data['used_memory'],
            'maxmemory' => (int)$data['maxmemory'],
            'used_memory_human' => $data['used_memory_human'] ?? '',
            'maxmemory_human' => $data['maxmemory_human'] ?? ''
        ];
    }

    // 统计的是运行周期(1分钟)内，请求的用户数量+未登录的IP数量，如果只要求用户数量，IP部分可以删去
    private function getActiveUsers(): int
    {
        $this->redis->zRemRangeByScore(CacheKey::activeUser(), 0, time() - 60);
        $count = $this->redis->zCard(CacheKey::activeUser());

        $this->redis->zRemRangeByScore(CacheKey::activeIp(), 0, time() - 60);
        $count += $this->redis->zCard(CacheKey::activeIp());

        return $count;
    }

    private function getRPM(): int
    {
        $this->redis->zRemRangeByScore(CacheKey::requestCountMinute(), 0, time() - 60);
        return $this->redis->zCard(CacheKey::requestCountMinute());
    }

    private function getErrorCount(): int
    {
        $this->redis->zRemRangeByScore(CacheKey::exceptionCountMinute(), 0, time() - 60);
        return $this->redis->zCard(CacheKey::exceptionCountMinute());
    }
}